#include <crt_util.h>

#if (TARGET_OS == OS_WIN)

struct thread_ctx {
    void (*entry)(void* arg);
    void* arg;
    _thread_t self;
};

static UINT __stdcall _thread_cb(void* arg)
{
    struct thread_ctx* ctx_p;
    struct thread_ctx ctx;
    ctx_p = arg;
    ctx = *ctx_p;
    heap_free(ctx_p);
    ctx.entry(ctx.arg);

    return 0;
}

int _thread_create(_thread_t* tid, _thread_proc entry, void* arg)
{
    struct thread_ctx* ctx;
    int err;
    HANDLE thread;

    ctx = heap_malloc(sizeof(*ctx));
    if (ctx == NULL)
        return -1;

    ctx->entry = entry;
    ctx->arg = arg;

    /* Create the thread in suspended state so we have a chance to pass
     * its own creation handle to it */
    thread = (HANDLE)CreateThread(NULL, 0, _thread_cb, ctx, CREATE_SUSPENDED, NULL);
    if (thread == NULL) {
        heap_free(ctx);
    } else {
        err = 0;
        *tid = thread;
        ctx->self = thread;
        ResumeThread(thread);
    }

    if (!err)
        return 0;
    return -1;
}

int _thread_join(_thread_t* tid)
{
    if (WaitForSingleObject(*tid, INFINITE))
        return -1;

    CloseHandle(*tid);
    *tid = 0;
    return 0;
}

int _thread_equal(const _thread_t* t1, const _thread_t* t2)
{
    return *t1 == *t2;
}

_thread_id _get_cthreadid()
{
    return GetCurrentThreadId();
}
_thread_id _get_threadid(_thread_t* tid)
{
    return GetThreadId(*tid);
}

#elif (TARGET_OS == OS_POSIX)

static size_t thread_stack_size(void)
{
    struct rlimit lim;

    if (getrlimit(RLIMIT_STACK, &lim)) {
        abort();
    }

    if (lim.rlim_cur != RLIM_INFINITY) {
        lim.rlim_cur -= lim.rlim_cur % (rlim_t)getpagesize();
        if (lim.rlim_cur >= PTHREAD_STACK_MIN)
            return lim.rlim_cur;
    }
    return 0;
}

int _thread_create(_thread_t* tid, _thread_proc entry, void* arg)
{
    int err;
    size_t stack_size;
    pthread_attr_t* attr;
    pthread_attr_t attr_storage;

    attr = NULL;
    stack_size = thread_stack_size();

    if (stack_size > 0) {
        attr = &attr_storage;

        if (pthread_attr_init(attr))
            abort();

        if (pthread_attr_setstacksize(attr, stack_size))
            abort();
    }

    err = pthread_create(tid, attr, (void* (*)(void*))entry, arg);

    if (attr != NULL)
        pthread_attr_destroy(attr);

    return err;
}
int _thread_join(_thread_t* tid)
{
    return pthread_join(*tid, NULL);
}

int _thread_equal(const _thread_t* t1, const _thread_t* t2)
{
    return pthread_equal(*t1, *t2);
}

_thread_id _get_cthreadid(void)
{
    return (uint64_t)(uintptr_t)pthread_self();
}
_thread_id _get_threadid(_thread_t* tid)
{
    return (uint64_t)(uintptr_t)pthread_self();
}

#elif (TARGET_OS == OS_MACH)

static size_t thread_stack_size(void)
{
    struct rlimit lim;

    if (getrlimit(RLIMIT_STACK, &lim)) {
        abort();
    }

    if (lim.rlim_cur != RLIM_INFINITY) {
        lim.rlim_cur -= lim.rlim_cur % (rlim_t)getpagesize();
        if (lim.rlim_cur >= PTHREAD_STACK_MIN)
            return lim.rlim_cur;
    }
    return 0;
}

int _thread_create(_thread_t* tid, _thread_proc entry, void* arg)
{
    int err;
    size_t stack_size;
    pthread_attr_t* attr;
    pthread_attr_t attr_storage;

    attr = NULL;
    stack_size = thread_stack_size();

    if (stack_size > 0) {
        attr = &attr_storage;

        if (pthread_attr_init(attr))
            abort();

        if (pthread_attr_setstacksize(attr, stack_size))
            abort();
    }

    err = pthread_create(tid, attr, (void* (*)(void*))entry, arg);

    if (attr != NULL)
        pthread_attr_destroy(attr);

    return err;
}
int _thread_join(_thread_t* tid)
{
    return pthread_join(*tid, NULL);
}

int _thread_equal(const _thread_t* t1, const _thread_t* t2)
{
    return pthread_equal(*t1, *t2);
}
_thread_id _get_cthreadid(void)
{
    return (uint64_t)(uintptr_t)pthread_self();
}
_thread_id _get_threadid(_thread_t* tid)
{
    return (uint64_t)(uintptr_t)pthread_self();
}

#elif (TARGET_OS == OS_UNIX)

static size_t thread_stack_size(void)
{
    struct rlimit lim;

    if (getrlimit(RLIMIT_STACK, &lim)) {
        abort();
    }

    if (lim.rlim_cur != RLIM_INFINITY) {
        lim.rlim_cur -= lim.rlim_cur % (rlim_t)getpagesize();
        if (lim.rlim_cur >= PTHREAD_STACK_MIN)
            return lim.rlim_cur;
    }
    return 0;
}

int _thread_create(_thread_t* tid, _thread_proc entry, void* arg)
{
    int err;
    size_t stack_size;
    pthread_attr_t* attr;
    pthread_attr_t attr_storage;

    attr = NULL;
    stack_size = thread_stack_size();

    if (stack_size > 0) {
        attr = &attr_storage;

        if (pthread_attr_init(attr))
            abort();

        if (pthread_attr_setstacksize(attr, stack_size))
            abort();
    }

    err = pthread_create(tid, attr, (void* (*)(void*))entry, arg);

    if (attr != NULL)
        pthread_attr_destroy(attr);

    return err;
}
int _thread_join(_thread_t* tid)
{
    return pthread_join(*tid, NULL);
}

int _thread_equal(const _thread_t* t1, const _thread_t* t2)
{
    return pthread_equal(*t1, *t2);
}

_thread_id _get_cthreadid(void)
{
    return (uint64_t)(uintptr_t)pthread_self();
}
_thread_id _get_threadid(_thread_t* tid)
{
    return (uint64_t)(uintptr_t)pthread_self();
}

#endif
